#! /usr/bin/perl -w

no warnings 'experimental::smartmatch';

use File::Slurp;
use Data::Dumper;
use JSON::PP;

our $DEBUG = 0;

# Optionally you can pass a PID to match in which case you get that one only
our @MATCH_PIDS = ();
if (scalar(@ARGV) > 3) {
    @MATCH_PIDS = @ARGV[3..scalar(@ARGV)-1];
    @MATCH_PIDS = map { uc($_) } @MATCH_PIDS;
}

our $types = {
    'signed char' => 'SCHAR',
    'unsigned char' => 'UCHAR',
    'char' => 'UCHAR',
    'signed int' => 'SINT',
    'unsigned int' => 'UINT',
    'int' => 'UINT',
    'long' => 'SINT32',
    'signed long' => 'SINT32',
    'unsigned long' => 'UINT32'
};

our $sizes = {
    'signed char' => 1,
    'unsigned char' => 1,
    'char' => 1,
    'signed int' => 2,
    'unsigned int' => 2,
    'int' => 2,
    'long' => 4,
    'signed long' => 4,
    'unsigned long' => 4
};

our $ctypes = {
    'signed char' => 'char',
    'unsigned char' => 'unsigned char',
    'char' => 'char',
    'signed int' => 'short',
    'unsigned int' => 'unsigned short',
    'int' => 'unsigned short',
    'signed long' => 'long',
    'long' => 'long',
    'unsigned long' => 'unsigned long',
    'BITFIELD' => 'HANDLED SEPARATELY'
};

# These map from the C++ types!
our $formats = {
    'char' => '%x',
    'unsigned char' => '%x',
    'short' => '%d',
    'unsigned short' => '%u',
    'long' => '%ld',
    'unsigned long' => '%lu',
    'float' => "%.4f",
    'string?' => "%s"
};

our $blacklist = {
#    'KUEHLDAUER_HVB' => 1                      # Obsolete and dupe
};

# We sometimes get duplicate result names - in that case we dedupe them by added the ID that they are returned from
our $usedfunctionnames = {};
our $usedresultnames = {};


open (CODE, ">&=3") || die "Can't fdopen 3";
open (POLLLIST, ">&=4") || die "Can't fdopen 4";

binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
binmode(CODE, ":utf8");
binmode(POLLLIST, ":utf8");

# Read ECU definition json as created by parseecu.pl
my $tables;
{
    my $jsonpp = JSON::PP->new->utf8;
    my $json = read_file($ARGV[0]);
    $tables = $jsonpp->decode($json);
}

our $ECU = $ARGV[1] || $tables->{'ovmscode'} || "???";
our $ECUID = $ARGV[2] || $tables->{'ovmsextaddr'} || "??";
our $ECUDESC = $tables->{'ovmsdesc'} || "ECU: $ECU";

print STDERR "Generating OVMS config for ECU $ECU on id $ECUID: $ECUDESC\n";

# Dump header
foreach my $FD (*STDOUT, *CODE, *POLLLIST) {
    print $FD "
//
// Warning: don't edit - generated by generate_ecu_code.pl processing $ARGV[0]: $ECU $ECUID: $ECUDESC
// This generated code  makes it easier to process CANBUS messages from the $ECU ecu in a BMW i3
//";
}

print "

#define I3_ECU_${ECU}_TX                                                0x06F1${ECUID}
#define I3_ECU_${ECU}_RX                                                0x06${ECUID}F1";


$tables = $tables->{'tables'};
my $functions = $tables->{'SG_FUNKTIONEN'};

my $need_nl = 0;

# Read through the functions
foreach my $function (@$functions) {

    my $functionname = $function->{ARG};

    # Filter
    if (scalar(@MATCH_PIDS)) {
	print STDERR "checking " . 
       	next unless (uc($function->{ID}) ~~ @MATCH_PIDS);
    }
    
    # For now skip those that take arguments (which are actually some of the most interesting)
    $need_nl = 1;
    if ($function->{ARG_TABELLE} && $function->{ARG_TABELLE} ne '-') {
        if ($need_nl) { print "\n"; $need_nl = 0; }
	    print "\n// Skipping " . $functionname . " on " . $function->{ID} . " which takes arguments";
	    next;
    }

    # And skip those marked as obsolete
    if ($function->{INFO} =~ /^Dieser Job ist nicht mehr angefordert und wird nicht mehr/) {
        if ($need_nl) { print "\n"; $need_nl = 0; }
	    print "\n// Skipping " . $functionname . " on " . $function->{ID} . " INFO says it's obsolete";
    	next;
    }

    # And skip those blacklisted
    if ($blacklist->{$functionname}) {
        if ($need_nl) { print "\n"; $need_nl = 0; }
    	print "\n// Skipping " . $functionname . " on " . $function->{ID} . ": Blacklisted";
    	next;
    }

    if ($need_nl) { print "\n"; $need_nl = 0; }

    # Uniqueify function name (there are dupes, though I'm sure there shouldn't be)
    if ($usedfunctionnames->{$functionname}) {
        $functionname = $functionname . '_'. uc($function->{ID});
        print STDERR "De-duped function " . $function->{ARG} . " as $functionname\n";
    }
    $usedfunctionnames->{$functionname} = 1;


    my $desc = format_desc($function->{INFO_EN}, $function->{INFO});
    if ($DEBUG) {
        print Dumper($function);
        if ($function->{ARG_TABELLE} && $function->{ARG_TABELLE} ne '-') {
            print Dumper({ arg_table => $tables->{uc($function->{ARG_TABELLE})} });
        }
        if ($function->{RES_TABELLE} && $function->{RES_TABELLE} ne '-') {
            print Dumper({ res_table => $tables->{uc($function->{RES_TABELLE})} });
        }
    }
    printf "\n#define I3_PID_%s_%-49s %6s", $ECU, $functionname, $function->{ID};
    print "\n        // $desc" if ($desc ne '');

    # write the code
    printf(CODE "%-80s    // %s", "\n\n  case I3_PID_${ECU}_" . $functionname . ": {", $function->{ID});

    # Now deal with the results
    my $results = $function->{RES_TABELLE};
    $results = $tables->{uc($results)};
    if (! $results) {
	# Might just be a single result
        if ($function->{DATENTYP} && $function->{DATENTYP} ne '-') {
            $results = [ $function ];
            Dumper($results);
        }
    }
    if ($results) {
        my $offset = 0;
        my @codelines;
        foreach my $result (@$results) {
            print "\n";
            print STDERR Dumper({result => $result}) if ($DEBUG);
            my $desc = format_desc($result->{INFO_EN}, $result->{INFO});
            my $datatype = $result->{DATENTYP};
            my $mul = $result->{MUL};
            my $div = $result->{DIV};
            my $add = $result->{ADD};
            my $unit = $result->{EINHEIT};
            my $resultname = $result->{RESULTNAME};
            $resultname = $result->{NAME} unless ($resultname && $resultname ne "-");
            # De-dupe it
            if ($usedresultnames->{$resultname}) {
                print STDERR "De-duped result $resultname as " . uc($function->{ID}) . "_" . $resultname . "\n";
                $resultname =  $resultname . '_' . uc($function->{ID});
            }
            $usedresultnames->{$resultname} = 1;
            # Build the macro expansion including any manipulations.
            my $expr;
            my $ctype;
            if ($datatype =~ /^data\[(\d+)\]$/) {
                print "\n    // Can't yet generate code for $resultname of type $datatype at offset $offset. But we account for the $1 bytes";
                print "\n        // $desc" if ($desc ne '');
                $offset += $1;
            }
            elsif ($datatype =~ /^string\[(\d+)\]$/) {
                print "\n    // Can't yet generate code for $resultname of type $datatype, at offset $offset. But we account for the $1 bytes";
                print "\n        // $desc" if ($desc ne '');
                $offset += $1;                
            }
            elsif (! $ctypes->{$datatype} ) {
                print "\n    // Can't process $resultname - don't know type $datatype (*** this will mean all the following offsets are wrong!!! ****)";
            } else {
                # For a bitfield we can look up the bit definitions.
                my $isbitfield = 0;
                my $bits;
                if ($datatype eq "BITFIELD") {
                    $isbitfield = 1;
                    $bits = $tables->{$resultname};
                    # print Dumper({ "resultname" => $resultname, "datatype" => $datatype, "bitsdesc" => $bits});
                    if ($bits && $bits->[0]) {
                        $datatype = $bits->[0]->{DATENTYP};
                        print "\n    // $resultname is a BITFIELD of size $datatype.  We don't yet generate definitions for each bit, we treat as the host data type";
                        print "\n        // $desc" if ($desc ne '');
                        foreach my $bit (@$bits) {
                            print "\n            // " . $bit->{RESULTNAME} . ": Mask: " . $bit->{MASKE} . " - " . $bit->{INFO_EN};
                        }
                    } else {
                        $datatype = "unsigned char";
                        print "\n    // $resultname is a BITFIELD of unknown size.  We don't have definitions for each bit, and we GUESSED it is one byte ***";
                        print "\n        // $desc" if ($desc ne '');
                    }
                }

                $ctype = $ctypes->{$datatype};
                $expr = '(RXBUF_' . $types->{$datatype} . '(' . $offset . ')';
                if ($mul ne '-' && $mul != 1) {
                    $expr .= '*' . $mul . 'f';
                    $ctype = 'float';
                }
                if ($div ne '-' && $div != 1) {
                    $expr .= '/' . $div . 'f';
                    $ctype = 'float';
                }
                $expr .= (($add > 0) ? '+' : '') . $add if ($add ne '-' && $add != 0);
                    $expr .= ')';
                $offset += $sizes->{$datatype};

                printf "\n    #define I3_RES_%s_%-45s %s", $ECU, $resultname, $expr;
                printf "\n    #define I3_RES_%s_%-45s '%s'", $ECU, $resultname . '_UNIT', $unit if ($unit && $unit ne '-');
                printf "\n    #define I3_RES_%s_%-45s %s", $ECU, $resultname . '_TYPE', $ctype if ($ctype);
                print "\n        // $desc" if ($desc ne '');

                # code
                push @codelines, "\n    $ctype $resultname = $expr;\n";
                push @codelines, "        // $desc\n";
                if ($isbitfield) {
                    if ($bits && $bits->[0]) {
                        push @codelines, "        // $resultname is a BITFIELD of size $datatype.  We don't yet generate definitions for each bit, we treat as the host data type\n";
                        foreach my $bit (@$bits) {
                            push @codelines, "            // " . $bit->{RESULTNAME} . ": Mask: " . $bit->{MASKE} . " - " . $bit->{INFO_EN} . "\n";
                        }
                    } else {
                        push @codelines, "            // $resultname is a BITFIELD of unknown size.  We don't have definitions for each bit, and we GUESSED it is one byte ***\n";            
                    }
                }
                print STDERR "Don't have format for $ctype\n" unless ($formats->{$ctype});
                push @codelines, "    ESP_LOGD(TAG, \"From ECU %s, pid %s: got %s=" . ($isbitfield ? "%lx" : $formats->{$ctype}) . "%s\\n\", \"$ECU\", \"" . $functionname . "\", \"$resultname\", " . ($isbitfield ? "(unsigned long)" : "") . "$resultname, " . (($unit && $unit ne "-") ? "\"\\\"$unit\\\"\"" : "\"\"") . ");\n";
            }
        }
        # code
        print CODE "\n    if (datalen < $offset) {\n        ESP_LOGW(TAG, \"Received %d bytes for %s, expected %d\", datalen, \"I3_PID_${ECU}_" . $functionname . "\", $offset);\n        break;\n    }";
        print CODE "\n" . join('', @codelines);
    }

    # code
    print CODE "\n    // ==========  Add your processing here ==========\n";
    print CODE "    hexdump(rxbuf, type, pid);\n";

    print CODE "\n    break;\n  }";


    # polllist
    printf POLLLIST "\n%-120s %s",  "  //{ I3_ECU_${ECU}_TX, I3_ECU_${ECU}_RX, VEHICLE_POLL_TYPE_OBDIIEXTENDED, I3_PID_${ECU}_" . $functionname . ",", " {  0,  0,  0,  0 }, 0, ISOTP_EXTADR },   // " . $function->{ID}; 
}

print "\n";
print POLLLIST "\n";
print CODE "\n";


sub format_desc {
    my ($en, $de) = @_;
    
    my $desc = $en || '';
    if ($desc eq '') {
	$desc = $de || '';
    } else {
	$desc .= " / " . $de if ($de);
    }
    if ($desc && $desc ne '') {
	$desc =~ s/(.{1,110}|\S{111,})(?:\s[^\S\r\n]*|\Z)/$1\n        \/\/ /g if (length $desc >= 110);
	$desc =~ s/(\n        \/\/ )?$//;
    }
    return $desc;
}

#          'result' => {
#                        'MASKE' => '-',
#                        'INFO' => "\x{c3}\x{9c}berlastz\x{c3}\x{a4}hler Sch\x{c3}\x{bc}tz K1",
#                        'DATENTYP' => 'unsigned int',
#                        'MUL' => '1.0',
#                        'INFO_EN' => 'Overload counter contactor K1',
#                        'ADD' => '0.0',
#                        'RESULTNAME' => 'STAT_OVERLOAD_K1_WERT',
#                        'NAME' => '-',
#                        'L/H' => 'high',
#                        'DIV' => '1.0',
#                        'EINHEIT' => '-'
#                      }
